////////////////////// 正式进入网络部分
// 网络第一次初见 -- 一些知识总结和基础代码熟悉

#if 0 
************************  前提知识总结  ************************

1 . 网路中要识别一个唯一的主机是靠 : IP , IP标识唯一个主机(是完成任务的目标IP地址)
2 . 网络的本质就是 : 进程间的通信 !!! , 网络也是操作系统的一部分 !!!
    网路其实也是文件 , 并且一个主机中会有多个软件(都是进程)
3 . 什么是端口号 ??? 
    端口号是标识唯一一个进程的 , 即 : 目标主机上的所有软件 , 都是进程 , 软件都是在主机上运行吧 ??? 
    那我这个软件我怎么知道我要在哪个主机运行 ?? 我怎么知道我在这个主机上运行的软件就是我的 ?? 
    // 所以 : 通过 IP + 端口号 , IP确定唯一主机 , 端口号确定唯一进程 
    **  所以 , 要识别唯一的一个主机上的唯一的一个进程 : 必须是 IP+端口号  **
4.  套接字Socket  = IP + 端口号 
5.  在网络通信中要有网络序列 , 什么是网路序列 ?? 
    // 在网络中一律使用  ****** 大端 ******
    所以 , 网络中涉及转化的问题 , 本机小端就要转网络序列的大端 
6.  什么是大端 ?? 小端 ?? 
    // 口诀 : 记忆小端 : 小小小 ! 即 : 小权值位放在小地址处 , 为小端 
    // 即 : 低字节数据存放在低地址处 , 为小端 ! , 反之大端 
7.  注意 : 服务器绑定网络时 , 服务器不要绑定特定 IP , 我们要将服务器初始化绑定IP时设置为 : 0(INADDR_ANY)
    // 因为这样 , 服务器可以接受任意IP的主机的通信 , 实现跨网络通信 
8.  *****  关于绑定 :
    // 1. 服务器要显示绑定 , 因为服务器要知道谁和我通信 并且 IP和端口必须是众所周知且不能轻易改变的！
    // 2. 客户端要隐式绑定端口号  , 即 : 让系统自己分配 , 因为我们绑定可能冲突
9.  关于转换 : 
    // 1. 本地转网络 -- hton#(根据转什么填充最后 : 端口 , s  .... ) 
    // 2. 网络转本地 -- ntoh#

10. IP网络存储 4 字节就够了 , 所以实际的IP是 4字节(32比特位) 
    // 但是我们用户看到的是点分十进制( " 192.168.1.2 ") 这样 ,所以我们要灵活转换
        in_addr_t inet_network(const char *cp);
        char *inet_ntoa(struct in_addr in);

#endif

//////////////////  正式代码练习开始 :

// 部署服务器

#pragma once

#include <iostream>
#include <string>
#include <cstring>
// 网络 4 大剑客 -------- 4大头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//------------------------------

// 引入日志
#include "Log.hpp"
using namespace LogModel;

// 默认值
const uint16_t default_port = 8080;
const int default_fd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port = default_port)
        : _port(port), _sockdf(default_fd), _isrunning(false)
    {
    }

    // 初始化服务器
    void Init()
    {
        // 既然要通信 ,使用网路 , 用 Socket 套接字
        //  【1.】 创建套接字  -- socket
#if 0 
        int socket(int domain, int type, int protocol);
        //  *** 第一个参数 : 怎么通信 , 本地通信(域间通信) 还是 网络通信 , 一般都是这两个
        //                          AF_UNIX             AF_INET
        //  *** 第二个参数 : 通信类型 , 是UDP还是TCP
        //  *** 第三个参数 : 协议控制 , 一般有了前两个这个直接写 0 就行
#endif
        // 这个本质就是打开一个网络文件 , 返回值 : 成功文件描述符 ,失败-1+错误码
        _sockdf = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockdf < 0)
        {
            LOG(LogLevel::FATAL) << "socket error !";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success , socketfd : " << _sockdf;

#if 0 
        //创建套接字成功了 , 证明创建网络文件成功了 , 但是此时只有文件呀 , 最终网络有了一定是通信的 
        //所以 , 要通信对于服务器来说要不要知道我和谁通信 ?? 要 
        //那怎么就知道了呢 ??? 通过 IP+端口号
        // ******** 但是 , 对于服务器来说 , 显示绑定不推荐绑定特定 IP , 所以我们用系统随机分配 , 这样可以和任意IP客户端通信 
        //所以 , 对于一台服务器还要有端口号 

        //端口号是什么 ?? 2个字节表示的 , 即 : 16位比特位  
        //使用接口 : 
        int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        // *** 第一个参数 : 创建套接字的返回值 -- 即 : 网路文件的文件描述符 
        // *** 第二个参数 : 这里就是继承多态实现的一样 , struct sockaddr 是基类
                                                //   struct sockaddr_in -- 是网络通信
                                                //   struct sockaddr_un -- 是本地通信 
        //struct sockaddr_in :
        // 1. 第一个字段 : 是协议类型 , UDP还是TCP
        // 2. 第二个字段 : 端口号
        // 3. 第三个字段 : IP地址 
        // 4. 第四个字段 : 清0即可 
        // 所以 ,  该函数使用时 : 需要强转成对应类型 
        // *** 第三个参数 : 就是该结构体大小
#endif
        //  【2.】 绑定IP+端口号  -- bind --- > 绑定指定网路信息
        //[2.1] . 定义网络通信结构体 : struct sockaddr_in
        struct sockaddr_in local;
        // 2.1.1 结构体清0
        bzero(&local, sizeof(local)); // 特定内存块清 0
        // 2.1.2 填充网络信息
        local.sin_family = AF_INET; // 网络通信
        //**** 特别在注意 : 绑定时绑定成  0 , INADDR_ANY , 这样可以适配多个客户端主机 */
        local.sin_addr.s_addr = INADDR_ANY; // 0
        // 这个结构体是定义在内核的 , 也就是在本机的
        // 但是网络通信需要网路序列 -- 大端 , 所以要转换为网络序列
        // 本地转网络 , 因为端口是 16位比特位 , 短整型 , 所以 , s
        local.sin_port = htons(_port);

        //[2.2] 绑定  , 记住要强转 !! , 返回值 :成功 , 0 . 否则 -1+错误码
        int n = bind(_sockdf, (struct sockaddr *)&local, sizeof(local));
        if (n == 0)
        {
            LOG(LogLevel::INFO) << "bind success  , socketfd : " << _sockdf;
        }
        else
        {
            LOG(LogLevel::FATAL) << "bind error , socketfd : " << _sockdf;
            exit(2);
        }
    }

    // 服务器启动 --- 服务器就是一个死循环
    // 一直要接发消息
    void Start()
    {
        _isrunning = true;
        // 死循环
        // 服务器启动了干嘛呢 ??
        //  1. 接受客户端消息 2 . 收到后给客户端发回去
        while (_isrunning)
        {
// 1. 接受客户端消息
//  ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
//                     struct sockaddr *src_addr, socklen_t *addrlen);
#if 0 
             recvfrom 函数介绍 : 
             1. 第一个参数就是 文件描述符 (socket返回值)
             2. 需要把消息放哪里 , 是一个缓冲区 , 用户自定义
             3. 缓冲区大小
             4. flags -- 表示是否阻塞接受 , 0 表示阻塞(默认)
             5. 要知道谁给我发的消息吧 ??? 这里就可以知道,里面都包含了(ip,端口号..) -- 输出型参数能把信息带回来
             6. 5参数结构体大小
             7. 返回值 : 返回的是实际收到的数 !
#endif

            // 定义缓冲区
            char buffer[1024];
            // 缓冲区清0
            memset(buffer, 0, sizeof(buffer));
            // 【1.】 接受客户端消息 , 当做字符串 , 留一个位置放 \0
            //  谁给我发的消息啊 ?? 远端客户端
            //  定义客户端信息
            struct sockaddr_in peer; // 这里已经占位就行
            socklen_t len = sizeof(peer);
            ssize_t num = recvfrom(_sockdf, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            // 收到内容了 , 因为返回值是实际收到的字节数
            if (num > 0)
            {
                // 收到消息 , 我要知道谁发的吧 ???
                // 所以要知道 ip和端口号 , 上面已经把网络信息带回来了
                //  !! 但是是网络中的 , 所以要转换成本地主机 !!
                // 网络转本地
                uint16_t peer_port = ntohs(peer.sin_port); // 端口号 - s
                // IP转换 , 网络中的IP是 4字节的 ,我们要转换成点分十进制
                // 网络中的IP转成字符串风格 ,里面就会做大小端转换 , 不用我们管 !
                std::string peer_ip = inet_ntoa(peer.sin_addr);

                buffer[num] = 0; // 添加 \0
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;

                // 2. 收到后给客户端发回去
                std::string echo_string = "server echo@ ";
                echo_string += buffer;

                // 发送接口 --- sendto
                //  ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                //                      const struct sockaddr *dest_addr, socklen_t addrlen);

                // 发给谁啊 ??? 发给远端
                ssize_t send = sendto(_sockdf, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);
                if (send > 0)
                {
                    LOG(LogLevel::INFO) << "send message to [" << peer_ip << ":" << peer_port << "] success !";
                }
            }
        }
    }

    ~UdpServer() {}

private:
    int _sockdf;     // 套接字返回的文件描述符 -- 表示这个网络文件
    uint16_t _port;  // 端口号 , 即 : 2个字节表示 , 16位比特位 - uint16_t
    bool _isrunning; // 是否启动 ?
};
